Don't make the audience sit through all your typo-ing [Overview of implementing an extension for WebMatrix - and the complete source code for the Snippets sample]
When I wrote about WebMatrix's new extensibility features a couple of posts back, I said I'd share the complete source code to Snippets, one of the sample extensions in the gallery. In this post, I'll be doing that along with explaining the overall extension model in a bit more detail. To begin with, here's the code so those of you who want to follow along at home can do so:
[Click here to download the complete source code for the Snippets extension]
Background
I created the Snippets extension by starting from the "WebMatrix Extension" Visual Studio Project template I wrote about previously. When expanded, the project template automatically set up the right project references (i.e., Microsoft.WebMatrix.Extensibility.dll
) and build steps and created a functioning extension with a single Ribbon button. From there, creating Snippets was a simple matter of adding the functionality I wanted on top of that foundation. But more on that later - I want to go over the default code first.
The default project template extension
One key thing to notice is that the template-generated WebMatrixExtension
class derives from ExtensionBase
, a base class which implements WebMatrix's IExtension
interface and simplifies some of the work of dealing with it. In the interest of generality (and because it's just an interface), IExtension
doesn't do much beyond identifying a few required properties. ExtensionBase
builds on that to offer concrete collections for the IEnumerable(T) properties, automatically MEF Imports the IWebMatrixHost
interface, creates an OnWebMatrixHostChanged
override, and so on. Of course, none of this is rocket science and the decision to use ExtensionBase
is completely up to you. But the whole reason it exists to make your life easier, so I'd suggest at least giving it a chance.
In order for an extension to be loaded by WebMatrix, it needs to be located in the right directory (more on that in the previous post) and it needs to MEF Export the IExtension
interface. You might think that ExtensionBase
should do the latter for you, but it doesn't because that might restrict your own class hierarchy (i.e., intermediary classes would advertise themselves as extensions even though they're not). Therefore, subclasses like the template-generated WebMatrixExtension
class need to explicitly export IExtension
.
With the groundwork out of the way, the basic functionality of the example extension is to add a Ribbon button and handle activation of that button to open the current web site in the browser. Providing content for the Ribbon is as easy as adding instances implementing the relevant interfaces (IRibbonButton
, IRibbonMenuButton
, IRibbonGroup
, etc.) to the generated class's RibbonItemsCollection
. It's important to do this exactly once (typically in the extension's constructor) because subsequent changes are not honored (FYI, that may change in the future, but please don't count on it). Of course, you can show and hide Ribbon content whenever you wish; you just can't add or remove items after initialization. Again, there are simple, concrete subclasses for each of the relevant interfaces (IRibbonButton
->RibbonButton
, etc.) so you don't need to spend time implementing these simple things yourself. And just like before, using the "helper implementations" is completely optional.
Creating a RibbonButton
requires a label, an ICommand implementation, and (optionally) a small/large image. The template's sample includes a couple of images already configured properly to provide a working example. Wiring up the images correctly is standard WPF, but the pack URI syntax can be a little tricky and it's common to forget to change the build type of the image files to "Resource" - so that's already been done for you as a helpful reminder. ICommand
implementation, the template sample uses a simple DelegateCommand
class (also included). The sample DelegateCommand
is very much in line with other implementations of DelegateCommand or RelayCommand. (Feel free to use whatever version you'd like; the sample DelegateCommand
exists simply to avoid introducing a dependency on a third-party library.) As you'd expect, the ICommand
's CanExecute and CanExecuteChanged methods are used to dynamically enable/disable the button and its Execute method is called when the button is clicked.
Yeah, it takes a while to describe what's going on, but there's hardly any code at all!
The Snippets extension
With the foundation behind us, it's time to consider the Snippets extension itself - and for that, it's helpful to know what it does. Snippets was created with the typical demo scenario in mind: a presenter is showing off WebMatrix and wants to add a block of code to a document, but doesn't want to type it out in front of the audience because that can be slow and boring. Instead, he or she clicks on the Snippets button in the Ribbon, selects from a list of available snippets, and the relevant text is automatically inserted in the editor. Of course, individual snippets should be easy for the user to add or modify and they should allow small and large amounts of text as well as blank lines, etc..
There are probably a hundred ways you could build this extension; here's how I've done it:
-
Each snippet is stored as a text file (ex: "Empty DIV.txt") in the
Snippets
folder of the user'sDocuments
folder (i.e.,
). The file's name identifies the snippet and its contents get added when the snippet is used. You can have as many or few snippet files as you'd like; they're read when the extension is loaded and cached. If there aren't any snippet files (or the%USERPROFILE%\Documents\Snippets Snippets
folder doesn't exist), the extension provides a simple message with instructions for how to set things up.Aside: WebMatrix needs to be restarted in order for snippet changes to take effect. An obvious improvement would be for the Snippets extension to monitor the
Snippets
directory for changes and apply them on the fly. -
The Snippets user interface is a
RibbonMenuButton
which contains a collection ofRibbonButton
instances corresponding to each of the available snippets. When theRibbonMenuButton
is clicked, it automatically shows theIRibbonButton
instances from itsItems
collection in a small, drop-down menu. When a selection is made, the menu is automatically closed.Aside: A
RibbonSplitButton
could have been used if there was a scenario where the user could click the top half of the button to perform a similar action (like inserting a default snippet). -
The Snippets button only makes sense when the "Files" workspace is active (i.e., a document is being edited), so the extension listens to the
IWebMatrixHost.WorkspaceChanged
event and shows/hides its Ribbon button according to whether the new workspace is an instance of theIEditorWorkspace
interface.Aside: One of the bits of feedback we've gotten so far is that this scenario (i.e., "only available for a single workspace") is common and should be simplified. Yep, message received.
:) -
To insert text into the document, Snippets invokes the
Paste
command via theIWebMatrixHost.HostCommands
property. While it's possible to get and set editor text directly for more advanced scenarios, thePaste
command works nicely because the editor automatically updates the caret position, replaces selected text, etc.. The downside to using the (application-wide)Paste
command is that if the input focus isn't in the body of an open file, then the paste action will be directed elsewhere and won't work as it's meant to.Aside: The editor interfaces are almost rich enough to manage everything here and avoid using
Paste
. However, there were a couple of glitches when I tried which led me to use the simpler paste approach for the sample. -
Input focus aside, one thing that's clear is that Snippets can never be added without an open document visible. To determine if that's the case, Snippets uses an unnamed host command to populate an instance of
IEditorContainer
. If that fails or theIEditorExt
within is null, that means no document is open and Snippets knows to disable its Ribbon buttons.Unnamed host commands work just like named commands (i.e.,
Paste
), though they're a little harder to get to. TheHostCommands.GetCommand
method exists for this purpose and allows the caller to pass a GUID/ID for the command to return. The relevant code looks like this:/// <summary> /// Gets an IEditorExt instance if the editor is in use. /// </summary> /// <returns>IEditorExt reference.</returns> private IEditorExt GetEditorExt() { var editorContainer = new EditorContainer(); _editorContainerCommand = WebMatrixHost.HostCommands.GetCommand(new Guid("27a0f541-c86c-4f0b-b436-0b50bf9f7ef8"), 10); if (_editorContainerCommand != null) { _editorContainerCommand.Execute(editorContainer); } return editorContainer.Editor; }
Helper properties for this and other unnamed commands be added in the future. For now, FYI about a useful one.
:) -
Although the
RibbonButton
class supports aparameter
parameter for passing to theICommand
'sCanExecute
/Execute
methods to provide additional context, this value is not actually passed through in some cases.:( This bug was found too late to fix for the Beta, but the good news is that it's easy to work around by creating a closure to capture the relevant parameter information instead. If you're not familiar with this technique, it involves defining an anonymous method that references the desired data; the compiler automatically captures the necessary values and passes them along when the delegate gets called.Here's what it looks like in the sample (using LINQ to create the collection):
// Create buttons for each snippet var snippetsButtons = _snippets .Select(s => new RibbonButton( s.Key, new DelegateCommand( (parameter) => GetEditorExt() != null, (parameter) => { // parameter is (incorrectly) null, so add this indirection for now HandleSnippetInvoke(s.Value); }), s.Value, _snippetsImageSmall, _snippetsImageLarge));
-
It turns out there's a subtle bug because of the following code in the sample:
// Paste the snippet into the editor var paste = WebMatrixHost.HostCommands.Paste; if (paste.CanExecute(insertText)) { paste.Execute(insertText); }
Note that Snippets is invoking a special flavor of the
Paste
command by providing the text as theparameter
property for theCanExecute
/Execute
methods (meaning "paste this text, please"). However, the underlying editor code is returning aCanExecute
result based on whether or not it could paste from the clipboard (i.e., it's not honoring the meaning of the text parameter). Therefore, if the clipboard is empty or contains non-text data like a file,CanExecute
returnsfalse
and Snippets isn't able to insert text.The easy workaround is to copy some text to the clipboard so the underlying implementation will return
true
forCanExecute
and the specialized paste operation will be invoked.
Summary
Snippets is a small extension that builds on the default WebMatrix Extension project template to implement some useful (if limited) functionality. Its original purpose was to simplify demos, but if people find practical uses for it, that's great, too!
But whether or not people use it, the Snippets extension touches on enough interesting areas of extensibility that people can probably learn from it. If you're getting started with WebMatrix extensions and are looking for a "real world" sample, I hope Snippets can be helpful. If you have feedback or questions - about Snippets or more generally - please let me know!